package flexibilityDesign;

import ilog.concert.IloLinearNumExpr;
import ilog.concert.IloNumVar;
import ilog.concert.IloRange;
import ilog.cplex.IloCplex;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import util.Lang;
import util.NullOutputStream;

public class StructureDesignMultiCut {
	private double lcpm;// c_ij/pi_ij
	private int n;// demand number
	private int m;// resource number
	private int M;// scenario counts

	private double[][] demands;
	private double[] capacities;
	private double[][] c;// link cost
	private double[][] pi;// profit margin

	public StructureDesignMultiCut() {
	}

	public StructureDesignMultiCut(int n, int m, int M, double lcpm) {
		this.n = n;
		this.m = m;
		this.M = M;
		this.lcpm = lcpm;
	}

	private void initData() {
		this.capacities = Support.readCapacities(n, m);
		// double cap = BigDecimal.valueOf(n * 100)
		// .divide(BigDecimal.valueOf(m), 5, BigDecimal.ROUND_HALF_EVEN)
		// .doubleValue();
		// this.capacities = new double[m];
		// for (int j = 0; j < m; j++)
		// this.capacities[j] = cap;
		this.demands = Support.readDemands(M, n);
		this.c = Support.readLinkCost(n, m);
		this.pi = Support.readProfitMargin(n, m);
	}

	public void Lshaped() {
		this.initData();

		double pro = BigDecimal.ONE.divide(BigDecimal.valueOf(M), 5,
				BigDecimal.ROUND_HALF_EVEN).doubleValue();

		double LB = Double.NEGATIVE_INFINITY;
		double UB = Double.MAX_VALUE;
		double gap = Double.MAX_VALUE;

		try {
			long start = System.currentTimeMillis();

			IloCplex master = new IloCplex();
			master.setOut(new NullOutputStream());
			IloNumVar[][] Y = new IloNumVar[n][m];
			for (int i = 0; i < n; i++)
				for (int j = 0; j < m; j++)
					Y[i][j] = master.boolVar();
			int[][] YValue = new int[n][m];// Y value
			IloNumVar[] theta = master.numVarArray(M, -1000000, 1000000);
			double[] thetaValues = new double[M];
			for (int s = 0; s < M; s++)
				thetaValues[s] = -100000;
			double masterObjValue = 0;

			IloLinearNumExpr obj = master.linearNumExpr();
			IloLinearNumExpr sumY = master.linearNumExpr();
			for (int i = 0; i < n; i++) {
				for (int j = 0; j < m; j++) {
					obj.addTerm(c[i][j], Y[i][j]);
					sumY.addTerm(1, Y[i][j]);
				}
			}
			for (int s = 0; s < M; s++)
				obj.addTerm(pro, theta[s]);
			master.addMinimize(obj);

			master.addLe(sumY, 3 * n);

			// when clockType=2, time unit is clock second
			master.setParam(IloCplex.IntParam.ClockType, 2);
			master.setParam(IloCplex.DoubleParam.TiLim, 700);

			double currentGap = Double.MAX_VALUE;
			int sameGapCount = 1;
			double premasterObjValue = 0.0;
			int iter = 1;
			int delta = n / 2;
			while (true) {
				if (iter > 1) {
					IloRange trustRegion = null;
					if (sameGapCount > 2)
						trustRegion = this.addTrustRegion(YValue, Y, master,
								delta);

					/**
					 * dynamically adjust tolerance of solver for MIP
					 * **/
					// if (sameGapCount > 2) {
					// double mgap = master
					// .getParam(IloCplex.DoubleParam.EpGap);
					// double ngap = mgap / 10;
					// ngap = ngap < 0.0001 ? 0.0001 : ngap;
					// master.setParam(IloCplex.DoubleParam.EpGap, ngap);
					// } else {
					if (gap > 0.05)
						master.setParam(IloCplex.DoubleParam.EpGap, 0.05);
					else if (gap > 0.03)
						master.setParam(IloCplex.DoubleParam.EpGap, 0.03);
					else if (gap > 0.02)
						master.setParam(IloCplex.DoubleParam.EpGap, 0.02);
					else if (gap > 0.009)
						master.setParam(IloCplex.DoubleParam.EpGap, 0.009);
					else if (gap > 0.007)
						master.setParam(IloCplex.DoubleParam.EpGap, 0.007);
					else if (gap > 0.005)
						master.setParam(IloCplex.DoubleParam.EpGap, 0.005);
					else if (gap > 0.003)
						master.setParam(IloCplex.DoubleParam.EpGap, 0.003);
					else if (gap > 0.001)
						master.setParam(IloCplex.DoubleParam.EpGap, 0.002);
					else
						master.setParam(IloCplex.DoubleParam.EpGap, 0.001);
					// }

					// for each iteration, provide initial solution
					for (int i = 0; i < n; i++) {
						double[] initValue = new double[m];
						for (int j = 0; j < m; j++) {
							// initValue[j] = YValue[i][j];
							initValue[j] = 1.0;
						}
						master.addMIPStart(Y[i], initValue);
					}

					long masterStart = System.currentTimeMillis();
					if (master.solve()) {
						long masterEnd = System.currentTimeMillis();
						System.out.println("master solve time = "
								+ (masterEnd - masterStart));
						for (int i = 0; i < n; i++)
							for (int j = 0; j < m; j++)
								YValue[i][j] = (int) master.getValue(Y[i][j]);
						masterObjValue = master.getObjValue();
						if (masterObjValue == premasterObjValue) {
							master.setParam(IloCplex.DoubleParam.EpGap, 0.001);
						} else
							premasterObjValue = masterObjValue;

						if (masterObjValue > LB)
							LB = masterObjValue;

						thetaValues = master.getValues(theta);
					} else {
						System.out.println("master status: "
								+ master.getStatus());
					}
					if (trustRegion != null)
						master.getModel().remove(trustRegion);

				} else {
					// TODO generate initial solution
					for (int i = 0; i < n; i++)
						for (int j = 0; j < m; j++)
							if (j == i || (i != n - 1 && j == i + 1)
									|| (i == n - 1 && j == 0))
								YValue[i][j] = 1;
							else
								YValue[i][j] = 0;
				}

				BigDecimal originalObj = BigDecimal.ZERO;
				for (int i = 0; i < n; i++)
					for (int j = 0; j < m; j++)
						originalObj = originalObj.add(BigDecimal
								.valueOf(c[i][j] * YValue[i][j]));

				BigDecimal[][] EtAg = new BigDecimal[n][m];
				for (int i = 0; i < n; i++)
					for (int j = 0; j < m; j++)
						EtAg[i][j] = BigDecimal.ZERO;
				BigDecimal etAg = BigDecimal.ZERO;

				List<Double> differenceList = new ArrayList<Double>();
				List<IloRange> cuts = new ArrayList<>();

				for (int sm = 0; sm < M; sm++) {
					IloCplex dual = new IloCplex();
					dual.setOut(new NullOutputStream());
					IloNumVar[] phi = dual.numVarArray(m,
							Double.NEGATIVE_INFINITY, 0.0);
					IloNumVar[] sigma = dual.numVarArray(n,
							Double.NEGATIVE_INFINITY, 0.0);
					IloNumVar[][] gamma = new IloNumVar[n][m];
					for (int i = 0; i < n; i++)
						for (int j = 0; j < m; j++)
							gamma[i][j] = dual.numVar(Double.NEGATIVE_INFINITY,
									0.0);
					IloLinearNumExpr dobj = dual.linearNumExpr();
					for (int j = 0; j < m; j++)
						dobj.addTerm(capacities[j], phi[j]);
					for (int i = 0; i < n; i++)
						dobj.addTerm(demands[sm][i], sigma[i]);
					for (int i = 0; i < n; i++) {
						for (int j = 0; j < m; j++) {
							dobj.addTerm(YValue[i][j] * capacities[j],
									gamma[i][j]);

							// constraint
							dual.addLe(dual.sum(phi[j], sigma[i], gamma[i][j]),
									-pi[i][j]);
						}
					}
					dual.addMaximize(dobj);

					if (dual.solve()) {
						originalObj = originalObj
								.add(BigDecimal.valueOf(pro).multiply(
										BigDecimal.valueOf(dual.getObjValue())));

						double estim = 0.0;
						for (int j = 0; j < m; j++) {
							estim += dual.getValue(phi[j]) * capacities[j];
						}
						for (int i = 0; i < n; i++) {
							estim += dual.getValue(sigma[i]) * demands[sm][i];
						}
						for (int i = 0; i < n; i++) {
							for (int j = 0; j < m; j++) {
								if (YValue[i][j] == 1)
									estim += dual.getValue(gamma[i][j])
											* YValue[i][j] * capacities[j];
							}
						}

						double difference = estim - thetaValues[sm];
						if (difference > 0) {
							BigDecimal et = BigDecimal.ZERO;
							for (int j = 0; j < m; j++)
								et = et.add(BigDecimal.valueOf(dual
										.getValue(phi[j]) * capacities[j]));
							for (int i = 0; i < n; i++)
								et = et.add(BigDecimal.valueOf(dual
										.getValue(sigma[i]) * demands[sm][i]));

							BigDecimal[][] Et = new BigDecimal[n][m];
							for (int i = 0; i < n; i++) {
								for (int j = 0; j < m; j++) {
									Et[i][j] = BigDecimal
											.valueOf(dual.getValue(gamma[i][j]))
											.multiply(
													BigDecimal
															.valueOf(-capacities[j]));
									// EtAg[i][j] = EtAg[i][j].add(Et[i][j]);
								}
							}

							IloLinearNumExpr optcut = master.linearNumExpr();
							for (int i = 0; i < n; i++)
								for (int j = 0; j < m; j++)
									optcut.addTerm(Et[i][j].doubleValue(),
											Y[i][j]);
							// master.addGe(master.sum(optcut, theta[sm]),
							// et.doubleValue());

							int idx = 0;
							for (int i = 0; i < differenceList.size(); i++) {
								if (differenceList.get(i) >= difference) {
									idx = i;
									break;
								}
							}
							differenceList.add(idx, difference);
							IloRange range = master.ge(
									master.sum(optcut, theta[sm]),
									et.doubleValue());
							cuts.add(idx, range);
							// master.add(range);
						}

						// etAg = etAg.add(et);

					}
					dual.end();
				}

				// end scenario enumeration

				// System.out.println("iter " + iter + ":" + UB + "," + LB);
				if (UB > originalObj.doubleValue())
					UB = originalObj.doubleValue();

				/**
				 * add knapsack inequality
				 * **/
				// IloLinearNumExpr knapIneq = master.linearNumExpr();
				// for (int i = 0; i < n; i++) {
				// for (int j = 0; j < m; j++) {
				// EtAg[i][j] = EtAg[i][j].multiply(BigDecimal
				// .valueOf(pro));
				// knapIneq.addTerm(
				// Math.floor(c[i][j] - EtAg[i][j].doubleValue()),
				// Y[i][j]);
				// }
				// }
				// etAg = etAg.multiply(BigDecimal.valueOf(pro));
				// master.addLe(knapIneq, Math.floor(UB - etAg.doubleValue()));

				if (iter > 1)
					gap = BigDecimal
							.valueOf(UB - LB)
							.divide(BigDecimal.valueOf(-LB), 5,
									BigDecimal.ROUND_HALF_EVEN).doubleValue();
				// add half of the cuts to master
				int cutsize = cuts.size();
				cutsize = (gap > 0.02) ? cutsize : cutsize / 2;
				for (int i = 0; i < cutsize; i++) {
					master.add(cuts.get(i));
				}
				System.out.println("iter " + iter + ": " + gap);
				if (gap == currentGap) {
					sameGapCount++;
				} else {
					currentGap = gap;
					sameGapCount = 1;
				}
				long currentTT = System.currentTimeMillis() - start;
				System.out.println("current total time = " + currentTT);

				if (gap < 0.001 || currentTT > 3600000) {
					StringBuilder builder = new StringBuilder(
							"adding upper bound \r\n");
					System.out.println("iter " + iter + ":" + UB + "," + LB);
					builder.append("m/n/M = " + m + "/" + n + "/" + M + "\r\n");
					builder.append("iter " + iter + ":" + UB + "," + LB
							+ "\r\n");
					for (int i = 0; i < n; i++) {
						for (int j = 0; j < m; j++) {
							if (YValue[i][j] == 1) {
								System.out.print("(" + i + "," + j + ") \t");
								builder.append("(" + i + "," + j + ") \t");
							}
						}
						builder.append("\r\n");
						System.out.println();
					}
					long end = System.currentTimeMillis();
					double time = end - start;
					System.out.println("time consumed: " + time);
					System.out.println("iteration: " + iter);
					builder.append("time consumed: " + time + "\r\n");

					try {
						Writer writer = new BufferedWriter(new FileWriter(
								Lang.projectRoot()
										+ "\\data\\result\\record.txt", true));
						writer.write(builder.toString());
						writer.close();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}

					break;
				} else
					iter++;

			}
			master.end();

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private IloRange addTrustRegion(int[][] YValue, IloNumVar[][] Y,
			IloCplex master, int delta) {
		try {
			IloLinearNumExpr trustR = master.linearNumExpr();
			int oneC = 0;
			for (int i = 0; i < n; i++) {
				for (int j = 0; j < m; j++) {
					if (YValue[i][j] == 1) {
						trustR.addTerm(-1.0, Y[i][j]);
						oneC++;
					} else
						trustR.addTerm(1.0, Y[i][j]);
				}
			}
			IloRange trustRegion = master.addLe(trustR, delta - oneC);
			return trustRegion;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}
}
